package filesystem.server;

import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import filesystem.server.file.FileFactory;
import filesystem.server.file.FileHandler;
import filesystem.server.file.FileHost;
import filesystem.util.Messages;

/**
 * Classe de TimeOut.
 * 
 * Utilizada para a execução de um método dentro de um determinado tempo.
 */
class TimeOutRead implements Callable<String>{
	private BufferedReader reader;
	
	/**
	 * Cria o objeto que executará o método com time out.
	 * 
	 * @param reader Buffer em que é esperada uma mensagem.
	 */
	protected TimeOutRead(BufferedReader reader){
		this.reader = reader;
	}
	
	/**
	 * Método com time out
	 * 
	 * Este método é executado dentro do tempo determinado
	 * ou é gerada uma TimeoutException.
	 */
	@Override
	public String call() throws Exception{
		return this.reader.readLine();
	}
	
}

/**
 * Trata a conexão de cada requisição individualmente.
 * 
 * Ao receber uma conexão o servidor instancia esta classe e passa
 * ao objeto o controle da comunicação com o requisitante. 
 */
public class ServerConnection extends Thread {

	private static int millisecondsTimeOut = 60000;
	
	private Socket socket;
	private InputStreamReader dataInStream;
	private DataOutputStream dataOutStream;
	
	/**
	 * Instancia um tratador de conexão.
	 *
	 * @param socket Socket da conexão tratada.
	 */
	public ServerConnection(Socket socket) {
		this.socket = socket;	
		try {
			OutputStream out = socket.getOutputStream();
			dataOutStream = new DataOutputStream(out);
		} catch (IOException e) {
			e.printStackTrace();
		}
		try {
			dataInStream = new InputStreamReader(socket.getInputStream());
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * Thread de tratamento de conexões.
	 * 
	 * Trata a conexão de um cliente com o servidor de acordo com o tipo de
	 * requisição.
	 * 
	 * Mensagem de "READ":
	 * 	- read  path/to/file pipeID
	 * 
	 * Respostas:
	 * 	- ack ok pipeID path/to/file
	 * 		- Aguarda um mensagem de fim de leitura:
	 * 			- endRead ok
	 * 			- endRead fail
	 * 
	 * 	- ack fail BUSY 
	 * 
	 *  - ack fail NOT_EXISTS
	 *  
	 *  
	 * Mensagem de "CREATE":
	 *  - create path/to/file pipeID
	 *  
	 * Respostas:
	 *  - ack ok
	 *  - ack fail FILE_ALREADY_EXISTS
	 *   
	 *   
	 * Mensagem de "DELETE":
	 *  - delete path/to/file
	 *  
	 * Respostas:
	 *  - ack ok
	 *  - ack fail BUSY
	 *  
	 * Mensagem de "WRITE":
	 * 	- write path/to/file pipeID
	 * 
	 * Respostas:
	 * 	- ack ok
	 *  - ack fail BUSY
	 *  - ack fail NOT_EXISTS
	 *  
	 *  
	 * Caso uma mensagem inválida seja recebida.
	 * 
	 * Resposta:
	 * 	- ack fail INVALID_COMMAND
	 *  
	 */
	@Override
	public void run(){
		
		BufferedReader reader;
		PrintWriter writer;
		String input;
		String[] content;

		System.out.println("Server connection handler.");
		
		try {
			reader = new BufferedReader( dataInStream );
			writer = new PrintWriter(socket.getOutputStream(),true);
			input = reader.readLine();
			content = input.split("[ ]+");

			//Analiza a requisição:
			
			if(content[0].equals(Messages.create)){
				
				//Requisição de CREATE:
				
				System.out.println("Create request: " + input);
				
				String[] path = content[1].split("/");
				
				System.out.println("Creating: " + path[path.length - 1]);
				
				if(FileFactory.createFile(path[path.length - 1], content[1], content[2])){
					System.out.println("Create success.");
					writer.println(Messages.ack + " " + Messages.ok);
					writer.flush();
				}else{
					System.out.println("Create fail.");
					writer.println(Messages.ack + " " + Messages.fail + " " + Messages.alreadyExists);
					writer.flush();
				}
				
			}else if(content[0].equals(Messages.delete)){
				
				//Requisição de DELETE:
				
				System.out.println("Delete request: " + input);
				
				String[] path = content[1].split("/");
				
				if(FileFactory.deleteFile(path[path.length - 1])){
					System.out.println("Delete success.");
					writer.println(Messages.ack + " " + Messages.ok);
					writer.flush();
				}else{
					System.out.println("Delete fail.");
					writer.println(Messages.ack + " " + Messages.fail + " " + Messages.busy);
					writer.flush();
				}
				
			}else if(content[0].equals(Messages.read)){
				
				//Requisição de READ:
				
				String[] path = content[1].split("/");
				
				System.out.println("Read request: " + input);
				
				FileHandler file = FileFactory.getFile(path[path.length - 1]);
				if(file != null){
					if(file.readRequest()){
						ExecutorService executor = Executors.newSingleThreadExecutor();
						Future<String> task = executor.submit(new TimeOutRead(reader)); 
						FileHost hosts[] = file.getHosts();
						int index = 0;
						for(int i = 0; i < hosts.length; i++){
							if(hosts[i].getHost().equals(content[2])){
								index = i;
								break;
							}
						}
						writer.println(Messages.ack + " " + Messages.ok + " " + hosts[index].getHost() + " " + hosts[index].getPath());
						writer.flush();
						try{
							input = task.get(millisecondsTimeOut, TimeUnit.MILLISECONDS);
						
							String[] response = input.split("[ ]+");
						
							if(response[0].equals(Messages.endRead) && response[1].equals(Messages.ok)){
								file.addHost(content[1], content[2]);
								file.readRelease();
								System.out.println("Read success.");
							}else{
								System.out.println("Read fail.");
							}
						}catch(TimeoutException e){
							System.out.println("Time Out.");
						}catch (Exception e) {
							e.printStackTrace();
						}
					}else{
						System.out.println("Read fail: " + Messages.busy);
						writer.println(Messages.ack  + " " + Messages.fail + " " + Messages.busy);
						writer.flush();
					}
				}else{
					System.out.println("Read fail: " + Messages.notExists);
					writer.println(Messages.ack  + " " + Messages.fail + " " + Messages.notExists);
					writer.flush();
				}

			}else if(content[0].equals(Messages.write)){
				
				//Requisição de WRITE:
				
				String[] path = content[1].split("/");
				System.out.println("Write request: " + input);
				FileHandler file = FileFactory.getFile(path[path.length - 1]);
				if(file != null){
					if(file.writeRequest()){
						file.writeCommit(content[1], content[2]);
						System.out.println("Write success");
					}else{
						System.out.println("Read fail: " + Messages.busy);
						writer.println(Messages.ack  + " " + Messages.fail + " " + Messages.busy);
						writer.flush();
					}
				}else{
					System.out.println("Read fail: " + Messages.notExists);
					writer.println(Messages.ack  + " " + Messages.fail + " " + Messages.notExists);
					writer.flush();
				}
				
			}else{
				System.out.println("Op fail: " + Messages.invalidCommand);
				writer.println(Messages.ack  + " " + Messages.fail + " " + Messages.invalidCommand);
				writer.flush();
			}
			
			writer.close();
			reader.close();
			
		}catch (IOException e){
			e.printStackTrace();
		}
		try {
			this.dataInStream.close();
			this.dataOutStream.flush();
			this.dataOutStream.close();
			this.socket.close();
			System.out.println("Connection closed");
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}
